/*
 * Copyright (c) 2005, 2014 vacoor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */
package org.vacoor.nothing.web.service.support.storage;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.vacoor.nothing.common.io.MessageDigestWritableByteChannel;
import org.vacoor.nothing.common.io.MultiWritableByteChannel;
import org.vacoor.nothing.common.util.IOUtils;
import org.vacoor.nothing.web.service.FileStorageService;

import java.io.*;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.OverlappingFileLockException;

/**
 * * 文件存储服务
 * <p/>
 * 该存储服务与图片存储服务(主要指需要生成多个规格的场景{@link ImageLocalStorageServiceImpl})
 * 不同的是存储的是原始内容, 不做任何处理(相对于需要处理的图片),eg: pdf, doc 等
 * 因此这里采用 hash 方式来处理数据冗余问题
 * TODO 修改
 * File              HashedFile
 * id                   hash
 * name                 path
 * date                 size
 * ext/type             date
 * hash                 mark
 * <p/>
 * if (supported)
 * preupload?filename=&hash=     --> not exists HashedFile ? upload : not upload
 * { code: 200, id: '', name:'', size:'', path: '', url:''  },
 * { code: 203 }  do upload
 * <p/>
 * upload file:
 * hash = hash(file);
 * hf = hashedFileDao.find(hash);
 * hf exists  --> save file
 * not exists --> doUpload, save HashedFile, save File
 * { code: 200, id: '', name:'', size:'', path: '', url:''  },
 * <p/>
 * // kindeditor { error: 0, url: 'imageUrl', message: 'error message'}
 * upl?ret=code,path,url [codename,pathname,urlname,sizename,hashname]
 * <p/>
 * download file:
 * decode/dl?id=fid -302-> ul/dl?fname=fname&hash=hash ??
 * or
 * decode/dl/fid/name  -->
 * <p/>
 * delete file:
 * FileDao.delete
 * <p/>
 * job  hashedFile ref < 1 && mark = 1  do delete; ref < 1 do mark = 1;
 * hashedFile ref > 0 && mark = 1 do mark = 0; (ref align)
 * (lazy delete date ≈ 1 ~ 2 job cycle)
 * <p/>
 * TODO 思考分目录(按照日期 or hash/基数)
 *
 * @author vacoor
 */
//@Service
public class HashedFileLocalStorageServiceImpl extends DirectoryBasedStorageService implements FileStorageService {
    private static final Logger LOG = LoggerFactory.getLogger(HashedFileLocalStorageServiceImpl.class);
    private static final String TEMP_FILE_PREFIX = HashedFileLocalStorageServiceImpl.class.getName() + ".temp-file-";
    private static final String TEMP_FILE_SUFFIX = ".tmp";

    @Override
    public File readAsFile(String id) {
        File file = calcFile(id);
        return file != null ? file : null;
    }

    @Override
    public String store(InputStream is) throws IOException {
        // 这里是否可以以UUID命名存入, 如果MD5存在, 再删除, 否则修改保存, 可以减少一次 IO 读写操作
        File tempFile = File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX);
        FileChannel tempWritableChannel = new RandomAccessFile(tempFile, "rw").getChannel();  // rw channel
        MessageDigestWritableByteChannel digestWritableChannel = MessageDigestWritableByteChannel.of("MD5");

        try {
            // 转存流并计算 hash
            // 这里 tempFile 应该不会重复, 因此不加锁了
            IOUtils.flow(Channels.newChannel(is), MultiWritableByteChannel.on(digestWritableChannel, tempWritableChannel), true, false);

            final String hash = digestWritableChannel.getHashAsHex();
            if (!exists(hash)) {
                LOG.debug("Store File, Hash: {}", hash);

                doStore(hash, tempWritableChannel);
            } else {
                // 如果文件已经存在不重复保存
                LOG.debug("File is already exists, Hash: {}", hash);
            }

            return hash;
        } finally {
            if (tempWritableChannel.isOpen()) {
                IOUtils.close(tempWritableChannel);
            }
            tempFile.delete();
        }
    }

    /**
     * 使用给定 channel 及其 hash 存储该 channel 内容
     *
     * @param hash
     * @param sourceChannel
     */
    protected File doStore(final String hash, final FileChannel sourceChannel) throws IOException {
        File destination = calcFile(hash);
        FileChannel wChannel = new FileOutputStream(destination).getChannel();

        try {
            LOG.debug("获取 FileLock, Hash: {}", hash);
            // 尝试加锁, 避免多个线程同时写入同一文件(虽然大多数场景不用考虑)
            wChannel.lock();

            LOG.debug("Transfer To {}", destination);

            sourceChannel.transferTo(0, sourceChannel.size(), wChannel);
        } catch (OverlappingFileLockException ignore) {
            LOG.debug("获取 FileLock 失败, Hash: {}", hash);
            // 文件名已经是校验和如果是相同文件名那么肯定是相同文件
            // 如果已经锁定该文件而无法获取锁, 就不再写入直接返回
        } finally {
            wChannel.close(); // channel 关闭后lock自动释放
        }
        return destination;
    }

    private File calcFile(String hash) {
        return new File(calcBucketDir(hash), hash);
    }

    protected File calcBucketDir(String hash) {
        // TODO 根据 hash 简单分桶
        // getRealPath  /WEB-INF
        File dir = new File("");//storeBaseDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        return dir;
    }
}
